package emulator.analyzer.sid;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import emulator.analyzer.AddressDefinition;
import emulator.analyzer.util.AddressMapper;
import emulator.analyzer.util.CmpCommand;
import emulator.analyzer.util.CmpCommandMap;
import emulator.analyzer.util.FlowMapper;
import emulator.analyzer.util.RangeRelocation;
import emulator.analyzer.util.RelocationMapper;
import emulator.hardware.HwWord;

public class SidImage
{
	static Logger logger = LogManager.getLogger(SidImage.class.getName());
	private SidInfo sid_info;
	byte [] data;
	private int start_address;
	private int init_address;
	private int exec_address;
	public SidImage(SidInfo sid_info) {
		this.sid_info = sid_info;
		data = new byte [sid_info.getBlockSize()];
	}

	public int getBlockSize() {
		return data.length;
	}

	public int getStartAddress() {
		return start_address;
	}

	public byte[] getData() {
		return data;
	}

	public int getInitAddress() {
		return init_address;
	}

	public int getExecAddress() {
		return exec_address;
	}

	public SidInfo getInfo() {
		return sid_info;
	}

	void relink(HashMap<Integer, AddressDefinition> address_map, byte[] sid_data, RangeRelocation code_relocation, RangeRelocation dyn_relocation, int [] dyn_flow, Collection<RangeRelocation> relocations, CmpCommandMap cmp_map)
	{
		data = Arrays.copyOf(sid_data, sid_data.length);
		
		start_address = sid_info.getStartAddress() + code_relocation.getOffset();
		init_address = sid_info.getInitAddress() + code_relocation.getOffset();
		if (code_relocation.getRange().contains(sid_info.getExecAddress()))
			exec_address = sid_info.getExecAddress() + code_relocation.getOffset();
		else if (dyn_relocation.getRange().contains(sid_info.getExecAddress()))
			exec_address = sid_info.getExecAddress() + dyn_relocation.getOffset();
		else
		{
			logger.error("Unknown memory area for EXEC $"+new HwWord(sid_info.getExecAddress()));
		}
		
		List<Integer> addresses = new ArrayList<Integer>(address_map.keySet());
		Collections.sort(addresses);

		AddressMapper code_mapper = new RelocationMapper(-sid_info.getStartAddress());
		AddressMapper dyn_mapper = dyn_relocation != null ? new FlowMapper(code_mapper, dyn_flow) : null;
		
		Relocator reloc = new Relocator(code_relocation, dyn_relocation, relocations, cmp_map, code_mapper, dyn_mapper);
		
		for (int address : addresses)
		{
			AddressDefinition def = address_map.get(address);
			reloc.relocateAddress(address, def, reloc.getRelocationForAddress(def));
		}
	}

	private void printError(String message, int address, AddressDefinition def) 
	{
		logger.error(message);
		logger.error(def.print("   $"+new HwWord(address)+": "));
	}

	public String getTitle() {
		return sid_info.getTitle();
	}

	public String getAuthor() {
		return sid_info.getAuthor();
	}

	public String getYear() {
		return sid_info.getYear();
	}

	class Relocator 
	{
		private RangeRelocation code_relocation;
		private RangeRelocation dyn_relocation;
		private Collection<RangeRelocation> relocations;
		private CmpCommandMap cmp_map;
		private AddressMapper code_mapper;
		private AddressMapper dyn_mapper;

		public Relocator(RangeRelocation code_relocation,
				RangeRelocation dyn_relocation,
				Collection<RangeRelocation> relocations, CmpCommandMap cmp_map,
				AddressMapper code_mapper, AddressMapper dyn_mapper) {
			this.code_relocation = code_relocation;
			this.dyn_relocation = dyn_relocation;
			this.relocations = relocations;
			this.cmp_map = cmp_map;
			this.code_mapper = code_mapper;
			this.dyn_mapper = dyn_mapper;
		}

		private RangeRelocation getRelocationForAddress(AddressDefinition def) {
			RangeRelocation act_relocation = null;
			if (code_relocation.getRange().contains(def.getFullAddress()))
				act_relocation = code_relocation;
			else if (dyn_relocation != null && dyn_relocation.getRange().contains(def.getFullAddress()))
				act_relocation = dyn_relocation;
			else
			{
				for (RangeRelocation relocation : relocations)
				{
					if (relocation.getRange().contains(def.getFullAddress()))
					{
						act_relocation = relocation;
						break;
					}
				}
			}
			return act_relocation;
		}

		private void relocateAddress(int address, AddressDefinition def, RangeRelocation relocation)
		{
			if (relocation == null) 
			{
				printError("Error: Unknown address for relocation $" + new HwWord(def.getFullAddress()), address, def);
				return;
			}
			if (def.getFullAddress() + relocation.getOffset() < 0)
			{
				printError("Error: Relocation out of range $" + new HwWord(def.getFullAddress() + relocation.getOffset()), address, def);
				return;
			}
			AddressMapper mapper = ( (dyn_relocation != null && dyn_relocation.getRange().contains(address)) ? dyn_mapper : code_mapper);
			try {
				switch (def.getType())
				{
				case AddressDefinition.TYPE_ADR:
					mapValue(mapper, data, address, def.getFullAddress(), relocation, cmp_map, true);
					mapValue(mapper, data, address+1, def.getFullAddress(), relocation, cmp_map, false);
					break;
				case AddressDefinition.TYPE_LOW:
					mapValue(mapper, data, address, def.getFullAddress(), relocation, cmp_map, true);
					break;
				case AddressDefinition.TYPE_HIGH:
					mapValue(mapper, data, address, def.getFullAddress(), relocation, cmp_map, false);
					break;
				case AddressDefinition.TYPE_ZP:
					if (def.getFullAddress() + relocation.getOffset() > 255)
						printError("Error: ZP address out of range $" + new HwWord(def.getFullAddress() + relocation.getOffset()), address, def);
					else
						mapValue(mapper, data, address, def.getFullAddress(), relocation, cmp_map, true);
					break;
				}
			}
			catch (ArrayIndexOutOfBoundsException e)
			{
				printError("Memory index out of bounds exception "+e+" new value $"+ new HwWord(def.getFullAddress() + relocation.getOffset()), address, def);
			}
		}

		private void mapValue(AddressMapper mapper, byte [] data, int address, int value, RangeRelocation relocation, CmpCommandMap cmp_map, boolean is_low)
		{
			Collection<CmpCommand> cmps = cmp_map.get(address);
			int new_value = value+relocation.getOffset();
			if (cmps != null)
			{
				for (CmpCommand cmp : cmps)
				{
					AddressDefinition def = new AddressDefinition();
					def.setFullAddress(cmp.getAddressFromOperand(value, is_low));
					def.setInstruction(cmp.getInstruction());
					def.setInstructionAddress(cmp.getLocation());
					def.setType(is_low ? AddressDefinition.TYPE_LOW : AddressDefinition.TYPE_HIGH);
					relocateAddress(cmp.getOperandAddress(), def, relocation);
				}
			}
			if (!is_low)
				new_value >>= 8;
			mapper.set(data, address, new_value & 0xff);
		}
	}
}
